<?php

namespace esp\core;

use esp\core\db\Mongodb;
use esp\core\db\Mysql;
use esp\core\db\Yac;

/**
 * Class Model
 * @package esp\core
 *
 * func_get_args()
 */
class Model
{
    private $_table_fix = 'tab';//表前缀
    private $_page_key = 'p';//分页，页码键名，可以任意命名，只要不和常用的别的键冲突就可以
    private $_app_name = 'PayCenter';//仅用于缓存表结构时和其他系统的区分，无别的用处
    private $__table = null;//创建对象时，或明确指定当前模型的对应表名
    private $__pri = null;//同上，对应主键名
    private $__cache = false;//是否缓存，若此值被设置，则覆盖子对象的相关设置
    private $__conf;
    public $_controller;

    /**
     * Model constructor.
     * @param bool $cache
     */
    final public function __construct(bool $cache = false)
    {
        $this->__cache = $cache;
        $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 2);
        if (isset($backtrace[1]) and isset($backtrace[1]['object'])) {
            $_controller = $backtrace[1]['object'];
            if (is_subclass_of($_controller, Model::class)) {
                $this->_controller = $_controller->_controller;
            } else if (!is_subclass_of($_controller, Controller::class)) {
                error('Model must be created in Controller or Model,This is `' . get_class($_controller) . '`');
            } else {
                $this->_controller = $_controller;
            }
            $this->_controller->debug('New(' . get_class($this) . ')', $backtrace[0]);
        } else {
            error('Model must be created in Controller,This is NULL.');
        }
        $this->__conf = Config::get('database.mysql');
        if (method_exists($this, '_init') and is_callable([$this, '_init'])) {
            call_user_func_array([$this, '_init'], []);
        }
    }

    /**
     * @param array ...$action
     * @return mixed
     */
    final public function error(...$action)
    {
        $pre = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
        if (count($action) === 1) $action = $action[0];
        return $this->_controller->error($action, $pre);
    }

    /**
     * @param array ...$action
     * @return Debug|mixed
     */
    final public function debug(...$action)
    {
        if (empty($action)) return $this->_controller->_debug;
        $pre = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
        if (count($action) === 1) $action = $action[0];
        return $this->_controller->debug($action, $pre);
    }

    /**
     * @param $action
     * @return mixed
     */
    final public function mysql_log($action)
    {
        return $this->_controller->debug_mysql($action, 'null');
    }

    /**
     * @param bool $cache
     * @return $this
     */
    final public function cache(bool $cache = false)
    {
        $this->__cache = $cache;
        return $this;
    }


    /**
     * @param int $i
     * @return Mysql
     * @throws \Exception
     */
    final protected function mysql(int $i = 0)
    {
        static $mysql = [];
        if (!isset($mysql[$i])) {
            $mysql[$i] = new Mysql($i, $this->__conf, $this);
        }
        return $mysql[$i];
    }

    /**
     * @return Mongodb
     */
    final protected function mongodb(string $db = 'temp')
    {
        static $mongodb = [];
        if (!isset($mongodb[$db])) $mongodb[$db] = new Mongodb(Config::get('database.mongodb'), $db);
        return $mongodb[$db];
    }

    /**
     * @param string $tab
     * @return Yac
     */
    final protected function yac(string $tab = 'tmp')
    {
        static $yac = [];
        if (!isset($yac[$tab])) $yac[$tab] = new Yac($tab);
        return $yac[$tab];
    }

    //===================================================

    /**
     * 当前模型对应的表名
     * @return Model|string
     */
    final public function table(string $table = null)
    {
        if (!is_null($table)) {
            $this->__table = $table;
            return $this;
        }
        if (!is_null($this->__table)) return $this->__table;

        static $val;
        if (!is_null($val)) return $val;
        if (isset($this->_table)) return $this->_table;

        preg_match('/(.+\\\)?(\w+)model$/i', get_class($this), $mac);
        if (!$mac) return null;

        return $val = ($this->_table_fix . ucfirst($mac[2]));
    }

    /**
     * select COLUMN_NAME,ORDINAL_POSITION,IS_NULLABLE,DATA_TYPE,CHARACTER_MAXIMUM_LENGTH,CHARACTER_OCTET_LENGTH,NUMERIC_PRECISION,NUMERIC_SCALE,CHARACTER_SET_NAME,COLLATION_NAME ,COLUMN_TYPE,COLUMN_KEY,EXTRA ,COLUMN_COMMENT from INFORMATION_SCHEMA.Columns where table_name='tabAdmin' and table_schema='dbPayCenter';
     * 当前模型表对应的主键字段名
     * @return null
     * @throws \Exception
     */
    final public function PRI()
    {
        if (!is_null($this->__pri)) return $this->__pri;
        if (isset($this->_id)) return $this->_id;
        $table = $this->table();
        if (!$table) throw new \Exception('Unable to get table name');
        $val = $this->mysql()->table('INFORMATION_SCHEMA.Columns')->select('COLUMN_NAME')->where(['table_name' => $table, 'EXTRA' => 'auto_increment'])->get()->row();
        if (empty($val)) return null;
        return $val['COLUMN_NAME'];
    }

    /**
     * 列出表字段
     * @return array
     * @throws \Exception
     */
    final public function field()
    {
        $table = $this->table();
        if (!$table) throw new \Exception('Unable to get table name');
        $val = $this->mysql()->table('INFORMATION_SCHEMA.Columns')->where(['table_schema' => $this->__conf['db'], 'table_name' => $table])->get()->rows();
        if (empty($val)) throw new \Exception("Table '{$table}' doesn't exist");
        return $val;
    }

    final public function delete_cache(...$key)
    {
        $table = $this->table();
        foreach ($key as $i => $val) {
            $this->yac("K{$table}")->del($val);
            $this->yac("C{$table}")->del($val);
        }
        return $this;
    }

    /**
     * 列出所有字段的名称
     * @return array
     * @throws \Exception
     */
    final public function title()
    {
        $table = $this->table();
        $data = $this->yac("TableField")->get($table);
        if (!empty($data)) return $data;
        if (!$table) throw new \Exception('Unable to get table name');
        $val = $this->mysql()->table('INFORMATION_SCHEMA.Columns')->select('COLUMN_NAME as field,COLUMN_COMMENT as title')->where(['table_name' => $table])->get()->rows();
        if (empty($val)) throw new \Exception("Table '{$table}' doesn't exist");
        $this->yac("TableField")->set($table, $val);
        return $val;
    }

    /**
     * @param $keyValue
     * @param bool $fromCache
     * @return array|bool
     * @throws \Exception
     */
    final public function read($keyValue, bool $fromCache = true)
    {
        $table = $this->table();
        if (!isset($this->_key)) throw new \Exception('Model _key Undefined');

        if ($fromCache and $this->__cache === true) {
            $data = $this->yac("K{$table}")->get($keyValue);
            if (!empty($data)) return $data;
        }

        $val = $this->mysql()->table($table)->where($this->_key, $keyValue)->get()->row();
        if ($this->__cache === true and !empty($val)) $this->yac("K{$table}")->set($keyValue, $val);
        return $val;
    }


    /**
     * 增
     * @param array $data
     * @param bool $full 传入的数据是否已经是全部字段，如果不是，则要从表中拉取所有字段
     * @return int
     * @throws \Exception
     */
    final public function insert(array $data, bool $full = false)
    {
        $table = $this->table();
        if (!$table) throw new \Exception('Unable to get table name');
        return $this->mysql(1)->table($table)->insert($full ? $data : $this->_FillField($table, $data));
    }

    /**
     * 新增行时，填充字段
     * @param $data
     * @return mixed
     */
    /**
     * @param string $table
     * @param array $data
     * @return array
     * @throws \Exception
     */
    final private function _FillField(string $table, array $data)
    {
        $field = $this->yac("{$this->_app_name}_table_field")->get($table);
        if (empty($field)) {
            $field = $this->field();
            $this->yac("{$this->_app_name}_table_field")->set($table, $field);
        }

        foreach ($field as $i => $rs) {
            if ($rs['EXTRA'] === 'auto_increment') continue;//自增字段
            if (isset($data[$rs['COLUMN_NAME']])) continue;//字段有值
            $string = array('CHAR', 'VARCHAR', 'TINYBLOB', 'TINYTEXT', 'BLOB', 'TEXT', 'MEDIUMBLOB', 'MEDIUMTEXT', 'LONGBLOB', 'LONGTEXT');
            $number = array('INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INTEGER', 'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL');
            if (in_array(strtoupper($rs['DATA_TYPE']), $number)) $data[$rs['COLUMN_NAME']] = 0;//数值型
            elseif (in_array(strtoupper($rs['DATA_TYPE']), $string)) $data[$rs['COLUMN_NAME']] = '';//文本型
            else $data[$rs['COLUMN_NAME']] = null;//其他类型，均用null填充，主要是日期和时间类型
        }
        return $data;
    }


    /**
     * 删
     * @param $where
     * @return mixed
     * @throws \Exception
     */
    final public function delete($where)
    {
        $table = $this->table();
        if (!$table) throw new \Exception('Unable to get table name');
        if (is_numeric($where)) {
            $where = [$this->PRI() => intval($where)];
        }
        return $this->mysql(1)->table($table)->where($where)->delete();
    }


    /**
     * 改
     * @param $where
     * @param array $data
     * @return bool|db\ext\Result|null
     * @throws \Exception
     */
    final public function update($where, array $data)
    {
        $table = $this->table();
        if (!$table) throw new \Exception('Unable to get table name');
        if (is_numeric($where)) {
            $where = [$this->PRI() => intval($where)];
        }
        return $this->mysql(1)->table($table)->where($where)->update($data);
    }

    /**
     * @param string $string
     * @return mixed
     * @throws \Exception
     */
    final public function quote(string $string)
    {
        return $this->mysql()->quote($string);
    }


    protected $pageCount = 0;
    protected $pageSize = 0;
    protected $pageIndex = 1;
    protected $pageSkip = 0;
    protected $tableJoin = [];
    protected $tableJoinCount = 0;
    protected $groupKey = null;
    protected $selectKey = null;

    final public function join(...$data)
    {
        if (empty($data)) {
            $this->tableJoin = [];
            return $this;
        }
        $this->tableJoin[] = $data;
        return $this;
    }

    final public function group(string $groupKey)
    {
        $this->groupKey = $groupKey;
        return $this;
    }

    final public function select($select, $add_identifier = true)
    {
        $this->selectKey = [$select, $add_identifier];
        return $this;
    }


    /**
     * 选择一条记录
     * @param $where
     * @param string|null $orderBy
     * @param string $sort
     * @return array|bool
     * @throws \Exception
     */
    final public function get($where, string $orderBy = null, string $sort = 'asc')
    {
        $table = $this->table();
        if (!$table) throw new \Exception('Unable to get table name');
        if (is_numeric($where)) {
            if ($this->__cache === true) {
                $kID = $where;
                $data = $this->yac("C{$table}")->get($kID);
                if (!empty($data)) return $data;
            }
            $where = [$this->PRI() => intval($where)];
        }
        $obj = $this->mysql()->table($table);

        if ($this->selectKey) $obj->select(...$this->selectKey);
        if (!empty($this->tableJoin)) {
            foreach ($this->tableJoin as $join) {
                $obj->join(...$join);
            }
        }
        if ($where) $obj->where($where);
        if ($this->groupKey) $obj->group($this->groupKey);

        if ($orderBy) {
            $sort = (strtolower($sort) === 'asc') ? 'asc' : 'desc';
            $obj->order($orderBy, $sort);
        }

        $data = $obj->get()->row();
        if ($this->__cache === true and isset($kID) and !empty($data)) {
            $this->yac("C{$table}")->set($kID, $data);
        }
        return $data;
    }

    /**
     * @param array $where
     * @param string|null $orderBy
     * @param string $sort
     * @param int $limit
     * @return array
     * @throws \Exception
     */
    final public function all($where = [], string $orderBy = null, string $sort = 'asc', int $limit = 0)
    {
        $table = $this->table();
        if (!$table) throw new \Exception('Unable to get table name');
        $obj = $this->mysql()->table($table);

        if ($this->selectKey) $obj->select(...$this->selectKey);
        if (!empty($this->tableJoin)) {
            foreach ($this->tableJoin as $join) {
                $obj->join(...$join);
            }
        }
        if ($where) $obj->where($where);
        if ($this->groupKey) $obj->group($this->groupKey);

        if ($orderBy) {
            $sort = (strtolower($sort) === 'asc') ? 'asc' : 'desc';
            $obj->order($orderBy, $sort);
        }
        return $obj->get($limit)->rows();
    }


    /**
     * @param null $where
     * @param string $ascDesc
     * @return array
     * @throws \Exception
     */
    final public function list($where = null, $ascKey = null, string $ascDesc = 'desc')
    {
        $table = $this->table();
        if (!$table) throw new \Exception('Unable to get table name');
        if ($this->pageSize === 0) $this->pageSet();
        $rs = $this->mysql()->table($table);
        if ($this->selectKey) $rs->select(...$this->selectKey);
        if (!empty($this->tableJoin)) {
            foreach ($this->tableJoin as $join) {
                $rs->join(...$join);
            }
        }
        if ($where) $rs->where($where);
        if ($this->groupKey) $rs->group($this->groupKey);
        if (is_array($ascKey)) {
            list($key, $asc) = $ascKey;
        } elseif (is_null($ascKey)) {
            list($key, $asc) = [$this->PRI(), $ascDesc];
        } else {
            if (in_array(strtolower($ascKey), ['asc', 'desc'])) {
                list($ascKey, $ascDesc) = [$this->PRI(), $ascKey];
            }
            list($key, $asc) = [$ascKey, $ascDesc];
        }
        $val = $rs->limit($this->pageSize, $this->pageSkip)->order($key, $asc)->get();
        $this->pageCount = $val->count();
        return $val->rows();
    }


    final public function pageSet(int $size = 10, int $index = 0)
    {
        $this->pageIndex = $index ?: Input::get($this->_page_key, 1);
        $this->pageSize = $size;
        $this->pageSkip = ($this->pageIndex - 1) * $this->pageSize;
        return $this;
    }


    /**
     * 组合分页连接
     * @param bool $mustRecode
     * @return mixed
     */
    final public function pageGet(bool $mustRecode = true)
    {
        $info = [
            'recode' => $this->pageCount,//记录数
            'size' => max(2, $this->pageSize),//每页数量
            'index' => $this->pageIndex,//当前页码
        ];
        if ($mustRecode and !$info['recode']) return '';

//        $css = ['ul' => 'pagination', 'li' => 'paginate_button', 'more' => 'more', 'active' => 'active'];
        $css = [
            'ul' => 'layui-box pageLink',
            'li' => 'layui-btn layui-btn-primary layui-btn-small list-btn',
            'more' => 'more',
            'active' => 'layui-btn-danger'
        ];

        $key = $this->_page_key;  //URL中标识页码的键名，可以任意指定，但不要和网站其他可能的参数重名
        $QueryString = '[QueryString]';//页面参数临时字符串
        $_show = 5;             //页码显示为当前页前后页数

        $info['index'] = $info['index'] ?: Input::get($key, 1);//当前页码

        $info['last'] = (int)($info['recode'] % $info['size']);//最后一页数
        $info['page'] = (int)($info['recode'] / $info['size']);
        $info['page'] += !!$info['last'] ? 1 : 0;//总页数

        $info['prev'] = $info['index'] - 1;//上一页
        $info['next'] = $info['index'] + 1;//下一页
        $info['prev'] < 1 and $info['prev'] = 1;
        if ($info['next'] > $info['page']) $info['next'] = $info['page'];

        $link = [];
        $link[] = "<ul class='{$css['ul']}'>";
        $link[] = "<li class='{$css['li']}'><a href='?{$key}=1&{$QueryString}'>首页</a></li>";
        $link[] = "<li class='{$css['li']}'><a href='?{$key}={$info['prev']}&{$QueryString}'>上一页</a></li>";

        $page = [];

        //页面导航的起止点
        $star = $info['index'] - $_show;
        $star < 1 and $star = 1;
        $stop = $info['index'] + $_show;
        $stop > $info['page'] and $stop = $info['page'];

        if ($star >= $_show) {
            $page[] = "<li class='{$css['li']} {$css['more']}'>...</li>";
        }

        for ($i = $star; $i <= $stop; $i++) {
            if ($i == $info['index'])
                $page[] = "<li class='{$css['li']} {$css['active']}'><a>{$i}</a></li>";
            else
                $page[] = "<li class='{$css['li']}'><a href='?{$key}={$i}&[QueryString]'>{$i}</a></li>";
        }

        if ($stop <= ($info['page'] - $_show)) {
            $page[] = "<li class='{$css['li']} {$css['more']}'><a>...</a></li>";
        }

        $link[] = implode($page);
        $link[] = "<li class='{$css['li']}'><a href='?{$key}={$info['next']}&{$QueryString}'>下一页</a></li>";
        $link[] = "<li class='{$css['li']}'><a href='?{$key}={$info['page']}&{$QueryString}'>末页</a></li>";
//        $link[] = "<li class='{$css['li']}'><a class='total'>共<span>{$info['recode']}</span>条/每页{$info['size']}条/共{$info['page']}页</a></li>";
        $link[] = "<li class='{$css['li']}'><a class='total'>第{$info['index']}/{$info['page']}页 每页{$info['size']}条/共{$info['recode']}条</li>";
        $link[] = "</ul>";

        $get = $_GET;
        unset($get[$key]);
        return str_replace([$QueryString], [http_build_query($get)], implode($link));
    }

    /**
     * 加载模型
     * 关于参数：在实际模型类中，建议用func_get_args()获取参数列表，也可以直接指定参数
     * @param $model
     * @param null $params
     * @return mixed
     */
    public static function &create(...$paras)
    {
        if (empty($paras)) return null;
        static $_Recode = [];
        $mod = $paras[0];
//        if (!preg_match('/^\w+$/', $mod)) error('模型名只可以是字母数字组合');
        $modKey = md5($mod);
        if (isset($_Recode[$modKey])) return $_Recode[$modKey];
        $from = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
        $ext = Config::system('frame.esp.modelExt');
        $dir = 'application/models/';
        $model = ucfirst(strtolower($mod));
        $class = 'pay\\' . $model . $ext;
//        load("{$dir}Base.php") and load("{$dir}{$model}.php") or error("模型文件{$model}不存在", $from);

        class_exists($class) or error("模型类[{$class}]不存在", $from);

        $_Recode[$modKey] = new $class();
        $_Recode[$modKey] instanceof Model or error("模型 [{$class}] 须继承自 [esp\\core\\Model]", $from);
        return $_Recode[$modKey];
    }


}